/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

package org.bhf.providers.security.authorization.xmlpolicy;

import org.bhf.security.authorization.ParameterizablePermission;
import org.bhf.security.authorization.Permissions;
import org.bhf.security.common.Role;
import org.bhf.security.common.UserID;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Principal;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;


/**
 * Policy implementation that is based on XML.
 */
final class XMLPolicy
{
    /** From principal (PrincipalHolder) to permission (PermissionCollection) */
    private final Map<PrincipalHolder,PermissionCollection>   grants  = new HashMap<PrincipalHolder,PermissionCollection>();

    /**
     * Construct an empty policy.
     */
    XMLPolicy()
    {

    }

    /**
     * Construct a <code>XMLPolicy</code> from the given <code>InputStream</code>.
     * @param in <code>InputStream</code> provides the XML policy definition.
     * @param close <code>true</code> to close the stream when done reading.
     * @throws org.xml.sax.SAXException ConversionError during parsing
     * @throws java.io.IOException IO error while reading or parsing the policy.
     */
    XMLPolicy( final InputStream in, final boolean close )
        throws SAXException, IOException
    {
        try
        {
            SAXParserFacility.factory.newSAXParser().parse( in, new SAXHandler() );
        }
        catch( ParserConfigurationException e )
        {
            throw new RuntimeException( e );
        }
        finally
        {
            if( close )
                in.close();
        }
    }

    /**
     * Retrieve the <code>Permission</code>s that have been assigned to the given
     * <code>Principal</code> by way of the policy associated with the current project.
     * The current project is determined by <code>ContectProject</code> and must be set
     * prior to invoking this operation.
     * @param permission Permission that determines the class of the returned permission.
     * @param principal <code>Principal</code> for which to retrieve the <code>Permission</code>s. Must not
     *      be <code>null</code>.
     * @return A <code>PermissionCollection</code> containing the relevant <code>Permission</code>s.
     */
    PermissionCollection     getPermissions( final Permission permission, final Principal principal )
    {
        return getPermissions( grants, permission, principal );
    }

    /** Grant a set of permission to the given <code>Role</code>.
     * @param role <code>Role</code> to which the permission should be granted. Must not be <code>null</code>
     * @param permissions <code>Permission</code>s to grant. Must not be <code>null</code>.
     */
    void                     assign( final Role role, final PermissionCollection permissions )
    {
        assert( role != null );
        assert( permissions != null );
        add( grants, new PrincipalHolder( role ), permissions );
    }

    /** Grant a set of permission to the given <code>userID</code>.
     * @param userID <code>userID</code> to which the permission should be granted. Must not be <code>null</code>
     * @param permissions <code>Permission</code>s to grant. Must not be <code>null</code>.
     */
    void                     assign( final UserID userID, final PermissionCollection permissions )
    {
        assert( userID != null );
        assert( permissions != null );
        add( grants, new PrincipalHolder( userID ), permissions );
    }

    /**
     * Write the policy as an XML stream. Includes an embedded DTD. This
     * method does not close the stream.
     * @param out XML output stream. Must not be <code>null</code>
     * @param close <code>true</code> to guarantee closing the writer.
     * @throws IOException ConversionError writing XML to stream
     */
    void                     writeAsXML( final OutputStream out, boolean close )
        throws IOException
    {
        final PrintStream writer = new PrintStream( out, true, "UTF-8" );

        try
        {
            writer.println( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" );
            writer.println( "" );
            writer.println( "<!DOCTYPE policy [" );
            writer.println( "    <!ELEMENT policy (assign*)>" );
            writer.println( "        <!ELEMENT assign ((role | user), permission+)>" );
            writer.println( "            <!ELEMENT role (#PCDATA)>" );
            writer.println( "            <!ELEMENT userID (#PCDATA)>" );
            writer.println( "            <!ELEMENT permission (class, name, action*)>" );
            writer.println( "                <!ELEMENT class (#PCDATA)>" );
            writer.println( "                <!ELEMENT name (#PCDATA)>" );
            writer.println( "                <!ELEMENT action (#PCDATA)>" );
            writer.println( "]>" );
            writer.println( "" );
            writer.println( "<policy>" );

            synchronized( grants )
            {
                for( final Object o : grants.entrySet() )
                {
                    final Map.Entry entry = (Map.Entry) o;
                    final PrincipalHolder princpalHolder = (PrincipalHolder) entry.getKey();
                    final PermissionCollection permissions = (PermissionCollection) entry.getValue();

                    writer.println( "  <assign>" );

                    if( !princpalHolder.isWildClass )
                    {
                        if( princpalHolder.principalClass.equals( Role.class ) )
                        {
                            if( princpalHolder.isWildName )
                                writer.println( "    <role>*</role>" );
                            else
                                writer.println( "    <role>" + princpalHolder.principalName + "</role>" );
                        }
                        else if( princpalHolder.principalClass.equals( UserID.class ) )
                        {
                            if( princpalHolder.isWildName )
                                writer.println( "    <user>*</user>" );
                            else
                                writer.println( "    <user>" + princpalHolder.principalName + "</user>" );
                        }
                    }

                    for( Enumeration j = permissions.elements(); j.hasMoreElements(); )
                    {
                        final Permission permission = (Permission) j.nextElement();
                        writer.println( "    <permission>" );
                        writer.println( "      <class>" + permission.getClass().getName() + "</class>" );
                        writer.println( "      <name>" + permission.getName() + "</name>" );
                        for( StringTokenizer actions = new StringTokenizer( permission.getActions(), "," ); actions.hasMoreTokens(); )
                            writer.println( "      <action>" + actions.nextToken().trim() + "</action>" );
                        writer.println( "    </permission>" );
                    }

                    writer.println( "  </assign>" );
                }
            }

            writer.println( "</policy>" );
        }
        finally
        {
            if( close )
                out.close();
        }
    }

    //
    // Private
    //

    /** Grant a set of permission to the given <code>Principal</code>.
     * @param map grants or denies Map.
     * @param principal <code>Principal</code> to which the permission should be granted. Must not be <code>null</code>
     * @param permissions <code>Permission</code>s to grant. Must not be <code>null</code>.
     */
    private static void                     add( final Map<PrincipalHolder,PermissionCollection> map, final PrincipalHolder principal, final PermissionCollection permissions )
    {
        assert( principal != null );
        assert( permissions != null );

        synchronized( map )
        {
            final PermissionCollection originalPermissions = map.get( principal );
            if( originalPermissions != null )
            {
                meld( originalPermissions, null, permissions );
            }
            else
            {
                map.put( principal, permissions );
            }
        }
    }

    private static PermissionCollection     getPermissions( final Map<PrincipalHolder,PermissionCollection> map, final Permission permission, final Principal principal )
    {
        final PermissionCollection  permissions = new Permissions();
        final Class                 clazz       = permission.getClass();

        if( principal != null )
        {
            if( principal instanceof Role )
                meld(
                    permissions,
                    clazz,
                    paramterizeWithRole( map.get( new PrincipalHolder( principal ) ), (Role)principal )
                );
            else
                meld(
                    permissions,
                    clazz,
                    map.get( new PrincipalHolder( principal ) )
                );
        }

        return permissions;
    }

    private static PermissionCollection     paramterizeWithRole( final PermissionCollection permissions, final Role role )
    {
        final Map parameters = role.getParameters();
        if( parameters.size() == 0 )
            return permissions;

        final PermissionCollection newPermissions = new Permissions();
        for( final Enumeration i = permissions.elements(); i.hasMoreElements(); )
        {
            final Permission permission = (Permission)i.nextElement();

            // Is this a parameterizable permission with a matching role?
            if( (permission instanceof ParameterizablePermission) )
            {
                final ParameterizablePermission p       = (ParameterizablePermission)permission;
                @SuppressWarnings( "unchecked" )
                final Map<String,String[]>      typed   = parameters;
                final Permission[]              ps      = p.parameterize( typed );

                for( Permission p1 : ps ) newPermissions.add( p1 );
            }
            else
            {
                newPermissions.add( permission );
            }
        }

        return newPermissions;
    }

    private static void                     meld(
        final PermissionCollection permissions,
        final Class clazz,
        final PermissionCollection p
    )
    {
        if( p != null )
            for( Enumeration i = p.elements(); i.hasMoreElements(); )
            {
                final Permission permission = (Permission)i.nextElement();
                if( clazz == null || permission.getClass().equals( clazz ) )
                    permissions.add( permission );
            }
    }

    //
    // Inner classes
    //

    private class SAXHandler extends DefaultHandler
    {
        private final Class[]   PERMISSION_CTOR_ONE_ARG     = new Class[] { String.class };
        private final Class[]   PERMISSION_CTOR_TWO_ARGS    = new Class[] { String.class, String.class };

        private StringBuffer            value;

        private PermissionCollection    permissions;

        private Class                   principalClass;
        private String                  principalName;

        private Class                   permissionClass;
        private String                  permissionName;
        private StringBuffer            permissionActions;

        public void startElement( String namespaceURI, String sName, String qName, Attributes attrs )
            throws SAXException
        {
            value   = new StringBuffer();

            String eName = sName; // element name
            if( "".equals( eName ) ) eName = qName; // not namespaceAware

            if( "permission".equals( eName ) )
            {
                permissionActions   = new StringBuffer();
            }
        }

        public void characters( char buf[], int offset, int len )
            throws SAXException
        {
            String s = new String( buf, offset, len );
            value.append(s);
        }

        public void endElement( String namespaceURI, String sName, String qName )
            throws SAXException
        {
            String eName = sName; // element name
            if( "".equals( eName ) ) eName = qName; // not namespaceAware

            // Grant or Deny
            if( "role".equals( eName ) )
            {
                principalClass  = Role.class;
                principalName   = value.toString();
                if( principalName.equals( "*" ) )
                    principalName = null;
            }
            else if( "user".equals( eName ) )
            {
                principalClass  = UserID.class;
                principalName   = value.toString();
                if( principalName.equals( "*" ) )
                    principalName = null;
            }

            // Grant
            else if( "assign".equals( eName ) )
            {
                final PrincipalHolder       principal           = new PrincipalHolder( principalClass, principalName );
                final PermissionCollection  originalPermissions = grants.get( principal );

                if( originalPermissions != null )
                {
                    meld( originalPermissions, null, permissions );
                }
                else
                {
                    grants.put( principal, permissions );
                }

                permissions     = null;
                principalClass  = null;
                principalName   = null;
            }

            // Permission
            else if( "class".equals( eName ) )
            {
                try
                {
                    permissionClass = Class.forName( value.toString(), false, Thread.currentThread().getContextClassLoader() );
                }
                catch( Exception e )
                {
                    throw new SAXException( e );
                }
            }
            else if( "name".equals( eName ) )
            {
                permissionName = value.toString();
            }
            else if( "action".equals( eName ) )
            {
                final String action = value.toString();
                if( permissionActions.length() > 0 )
                    permissionActions.append( ',' );
                permissionActions.append( action );
            }
            else if( "permission".equals( eName ) )
            {
                if( permissions == null )
                    permissions = new Permissions();

                try
                {
                    if( permissionActions.length() == 0 )
                    {
                        final Constructor ctor = permissionClass.getConstructor( PERMISSION_CTOR_ONE_ARG );

                        permissions.add(
                            (Permission)ctor.newInstance( permissionName )
                        );
                    }
                    else
                    {
                        final Constructor ctor = permissionClass.getConstructor( PERMISSION_CTOR_TWO_ARGS );

                        permissions.add(
                            (Permission)ctor.newInstance( permissionName, permissionActions.toString() )
                        );
                    }
                }
                catch( Exception e )
                {
                    throw new SAXException( e );
                }
            }
        }
    }

    static final class PrincipalHolder
    {
        final Class                 principalClass;
        final String                principalName;
        final boolean               isWildName;
        final boolean               isWildClass;

        private PrincipalHolder( final Principal principal )
        {
            final String name = principal.getName();

            this.isWildClass    = false;
            this.isWildName     = name.equals( "*" );
            this.principalClass = principal.getClass();
            this.principalName  = isWildName ? null : name;
        }

        private PrincipalHolder( final Class principalClass, final String principalName )
        {
            if( principalClass == null && principalName != null )
                throw new IllegalArgumentException( "principalClass == null && principalName != null" );

            this.principalClass = principalClass;
            this.principalName  = principalName;
            this.isWildClass    = principalClass == null;
            this.isWildName     = principalName == null;
        }

        public boolean equals( Object o )
        {
            if( this == o ) return true;
            if( !(o instanceof PrincipalHolder) ) return false;

            final PrincipalHolder principalHolder = (PrincipalHolder)o;

            if( principalClass != null ? !principalClass.equals( principalHolder.principalClass ) : principalHolder.principalClass != null )
                return false;
            if( principalName != null ? !principalName.equals( principalHolder.principalName ) : principalHolder.principalName != null )
                return false;

            return true;
        }

        public int hashCode()
        {
            int result;
            result = (principalClass != null ? principalClass.hashCode() : 0);
            result = 29 * result + (principalName != null ? principalName.hashCode() : 0);
            return result;
        }
    }
}



